/* main.c / net.c (TCP) *//* [ ] 1. tcp_listen(port, &sock) — SO_REUSEADDR, non-blocking *//* [ ] 2. conn_t 와 conn 테이블 / pollfd 배열 (동적, 상한 1024) *//* [ ] 3. accept 처리: 가득이면 즉시 close + WRN, 아니면 배열에 추가 (realloc 은 tmp) *//* [ ] 4. conn_readable: recv → 버퍼에 이어 붙임 → 12바이트 이상이면 전체 길이 계산 → 모였으면 msg_unpack → push → 버퍼 앞으로 당김 (반복) *//* [ ] 5. 끊김 / 오류 처리: 상태 해제, fd close, 배열에서 제거 *//* [ ] 6. 종료: 모든 연결 close, 나머지는 A 와 같다 */
2-C. Level 3 (3회차, 백지)
/* 아무것도 안 본다. * §1-C 요구사항서와 §3 시나리오만 보고 만든다. * * 막히면 세 가지만 찾아본다. * - 슬롯 테이블을 한 뮤텍스로 보호하는 법 * - pthread_cond_timedwait 로 "1초마다 깨기 + 종료 때 즉시 깨기" 를 함께 하는 법 * - 감시 스레드를 join 한 뒤에 슬롯 테이블을 해제하는 순서 */
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 로 SYNOPSIS 맨 위의 #include 를 확인한다.
데몬이 멈추면 gdb -p <pid> → thread apply all bt 로 어느 스레드가 어디서 기다리는지 본다.
그래도 안 되면 모범답안의 해당 단계 하나만 본다.
3. 테스트 코드 — 이게 통과하면 끝
d7_daemon/src/jobq.h jobq.c 작업 큐 (변형 A, B, C 공통)d7_daemon/src/main.c net.c ... 데몬d7_daemon/test/test_jobq.c 큐 단위 시험 (C)d7_daemon/test/run.sh 데몬 시나리오 시험 (셸)
시험의 형태 (D1~D9 공통)
정상 한 가지, 경계 여러 가지, NULL / 실패 경로를 확인한다.
D7 에서는 큐는 C 단위 시험으로, 데몬은 시나리오로 나누어 확인한다.
큐가 틀린 채로 데몬 시나리오를 돌리면 어디가 틀렸는지 알 수 없다.
test_jobq 가 PASS 가 된 뒤에run.sh 를 돌린다.
3-0. 공통 매크로
#include <stdio.h>#include <stdlib.h>#include <string.h>#include "common.h"#include "log.h"log_level_t g_log_level = LOG_LV_ERR;static int g_fail = 0;#define T_EQ_INT(_got, _exp) \ do { \ long long _g = (long long)(_got), _e = (long long)(_exp); \ if (_g != _e) { g_fail++; \ printf(" X %s:%d got=%lld exp=%lld\n", \ __func__, __LINE__, _g, _e); } \ } while (0)
3-A. 큐 단위 시험 — test_jobq.c
#include <pthread.h>#include <sched.h>#include <stdatomic.h>#include <time.h>#include "jobq.h"static void nap_ms(long ms){ struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L }; nanosleep(&ts, NULL);}static void test_fifo(void){ jobq_t *q = NULL; job_t j; int i = 0; T_EQ_INT(jobq_create(4, &q), RET_OK); for (i = 0; i < 4; i++) { memset(&j, 0x00, sizeof(j)); j.session_id = (uint32_t)i; T_EQ_INT(jobq_push(q, &j), RET_OK); } memset(&j, 0x00, sizeof(j)); j.session_id = 99; T_EQ_INT(jobq_push(q, &j), RET_TOO_SMALL); /* 가득 참: 한 칸 넘김 */ for (i = 0; i < 4; i++) { T_EQ_INT(jobq_pop(q, &j), RET_OK); T_EQ_INT(j.session_id, i); /* 먼저 넣은 것이 먼저 나온다 */ } jobq_close(q); T_EQ_INT(jobq_push(q, &j), RET_NOT_SUPPORTED); /* 닫힌 큐 */ T_EQ_INT(jobq_pop(q, &j), RET_NOT_FOUND); /* 닫혔고 비었다 */ jobq_destroy(q);}static void test_drain_and_wrap(void){ jobq_t *q = NULL; job_t j; int round = 0, i = 0; T_EQ_INT(jobq_create(3, &q), RET_OK); for (round = 0; round < 100; round++) { /* head / tail 가 몇 바퀴 돈다 */ for (i = 0; i < 3; i++) { memset(&j, 0x00, sizeof(j)); j.session_id = (uint32_t)(round * 3 + i); T_EQ_INT(jobq_push(q, &j), RET_OK); } for (i = 0; i < 3; i++) { T_EQ_INT(jobq_pop(q, &j), RET_OK); T_EQ_INT(j.session_id, round * 3 + i); } } for (i = 0; i < 3; i++) { j.session_id = (uint32_t)i; jobq_push(q, &j); } jobq_close(q); for (i = 0; i < 3; i++) T_EQ_INT(jobq_pop(q, &j), RET_OK); /* ★ 닫혀도 남은 건 꺼낸다 */ T_EQ_INT(jobq_pop(q, &j), RET_NOT_FOUND); jobq_destroy(q);}static void test_null(void){ jobq_t *q = NULL; job_t j; memset(&j, 0x00, sizeof(j)); T_EQ_INT(jobq_create(0, &q), RET_INVALID_ARG); T_EQ_INT(q == NULL, 1); /* ★ 실패하면 NULL */ T_EQ_INT(jobq_create(4, NULL), RET_INVALID_ARG); T_EQ_INT(jobq_push(NULL, &j), RET_INVALID_ARG); T_EQ_INT(jobq_pop(NULL, &j), RET_INVALID_ARG); jobq_close(NULL); /* 죽지 않으면 통과 */ jobq_destroy(NULL);}/* ── 스레드 시험 ─────────────────────────────────────────────────── */static atomic_long g_sum;static atomic_int g_exited;static void *consumer(void *arg){ jobq_t *q = (jobq_t *)arg; job_t j; while (jobq_pop(q, &j) == RET_OK) atomic_fetch_add(&g_sum, (long)j.session_id); atomic_fetch_add(&g_exited, 1); return NULL;}static void *producer(void *arg){ jobq_t *q = (jobq_t *)arg; job_t j; int i = 0; memset(&j, 0x00, sizeof(j)); for (i = 1; i <= 5000; i++) { j.session_id = (uint32_t)i; while (jobq_push(q, &j) == RET_TOO_SMALL) sched_yield(); /* 가득 차면 잠깐 양보 */ } return NULL;}static void test_threads(void){ jobq_t *q = NULL; pthread_t c[4], p[2]; int i = 0; atomic_store(&g_sum, 0); T_EQ_INT(jobq_create(16, &q), RET_OK); for (i = 0; i < 4; i++) pthread_create(&c[i], NULL, consumer, q); for (i = 0; i < 2; i++) pthread_create(&p[i], NULL, producer, q); for (i = 0; i < 2; i++) pthread_join(p[i], NULL); jobq_close(q); /* 생산이 끝난 뒤에 닫는다 */ for (i = 0; i < 4; i++) pthread_join(c[i], NULL); T_EQ_INT(atomic_load(&g_sum), 2L * (5000L * 5001L / 2L)); /* ★ 유실도 중복도 없다 */ jobq_destroy(q);}static void test_close_wakes_all(void){ jobq_t *q = NULL; pthread_t c[4]; int i = 0, waited = 0; atomic_store(&g_exited, 0); T_EQ_INT(jobq_create(4, &q), RET_OK); for (i = 0; i < 4; i++) pthread_create(&c[i], NULL, consumer, q); /* 전부 빈 큐에서 대기 */ nap_ms(100); T_EQ_INT(atomic_load(&g_exited), 0); jobq_close(q); for (waited = 0; waited < 2000 && atomic_load(&g_exited) < 4; waited += 10) nap_ms(10); T_EQ_INT(atomic_load(&g_exited), 4); /* ★ broadcast 가 아니면 1개만 깬다 */ if (atomic_load(&g_exited) < 4) { printf(" X hang: skip join\n"); g_fail++; return; } for (i = 0; i < 4; i++) pthread_join(c[i], NULL); jobq_destroy(q);}int main(void){ test_fifo(); test_drain_and_wrap(); test_null(); test_threads(); test_close_wakes_all(); printf(g_fail ? "FAIL %d\n" : "PASS\n", g_fail); return g_fail ? 1 : 0;}
3-B. 데몬 시나리오 시험 — run.sh
시나리오는 같은 스크립트가 변형별로 다르게 돌린다 (A / B / C).
메시지는 D4 의 기대 바이트 벡터를 그대로 쓴다.
이름
바이트
뜻
VALID
22 85 00 00 01 02 03 04 0A 0B 0C 90 C2 D9
type=2, session=0x01020304, 정상
NOREPLY
29 00 00 00 01 02 03 04 0A 0B 0C 00 BC EE
type=9 (변형 C 의 무응답 요청)
#!/usr/bin/env bash# d7_daemon/test/run.sh — 데몬 시나리오 시험# 사용법: bash d7_daemon/test/run.sh <데몬 실행파일> [A|B|C]# 예) bash d7_daemon/test/run.sh build/d7_daemon_daemon A# 필요한 것: bash (/dev/udp, /dev/tcp), python3 (포트를 미리 잡아 두는 용도)BIN="$1"; VAR="${2:-A}"PORT="${PORT:-19000}"W="$(mktemp -d)"trap 'kill $(jobs -p) 2>/dev/null; rm -rf "$W"' EXITVALID='\x22\x85\x00\x00\x01\x02\x03\x04\x0a\x0b\x0c\x90\xc2\xd9'NOREPLY='\x29\x00\x00\x00\x01\x02\x03\x04\x0a\x0b\x0c\x00\xbc\xee'pass=0; fail=0check() { local m="$1"; shift; if "$@"; then echo " ok $m"; pass=$((pass+1)); else echo " X $m"; fail=$((fail+1)); fi; }eq() { [ "$1" = "$2" ]; }count() { grep -c "$1" "$W/log" || true; }no_asan() { ! grep -qE 'AddressSanitizer|LeakSanitizer|runtime error' "$W/log"; }mkcfg() { # mkcfg <worker> <log_level> [expire_sec] printf 'listen_port=%s\nworker_num=%s\nlog_level=%s\nexpire_sec=%s\n' \ "$PORT" "${1:-4}" "${2:-INF}" "${3:-5}" > "$W/d.cfg"}send() { # send <printf 형식> : A 는 UDP, B 는 TCP 한 번 연결 if [ "$VAR" = B ]; then { exec 3<>/dev/tcp/127.0.0.1/$PORT; printf "$1" >&3; exec 3>&-; } 2>/dev/null else printf "$1" > /dev/udp/127.0.0.1/$PORT 2>/dev/null fi}start() { "$BIN" "$W/d.cfg" 2> "$W/log" & PID=$!; sleep 0.5; }wait_exit() { # wait_exit <최대 초> → RC (종료 코드, 못 끝나면 "timeout") local i for ((i = 0; i < $1 * 10; i++)); do kill -0 $PID 2>/dev/null || break; sleep 0.1; done if kill -0 $PID 2>/dev/null; then RC=timeout; kill -9 $PID; wait $PID 2>/dev/null else wait $PID; RC=$?; fi}occupy() { # 같은 포트를 미리 잡아 둔다 local kind=DGRAM; [ "$VAR" = B ] && kind=STREAM python3 -c "import socket,sys,time; s=socket.socket(socket.AF_INET, socket.SOCK_$kind); s.bind(('127.0.0.1', int(sys.argv[1]))); [s.listen(1) for _ in [0] if '$kind' == 'STREAM']; time.sleep(30)" "$PORT" & BLOCK=$!; sleep 0.5}echo "[$VAR] 1. 정상 기동 → SIGINT"mkcfg; start; t0=$SECONDS; kill -INT $PID; wait_exit 5check "exit 0" eq "$RC" 0check "5초 이내" test $((SECONDS - t0)) -le 5check "ASan 리포트 없음" no_asanecho "[$VAR] 2. 설정 파일 없이 기동""$BIN" "$W/none.cfg" 2> "$W/log" & PID=$!; wait_exit 3check "exit 1" eq "$RC" 1check "스레드를 만들기 전에 종료 (started 없음)" eq "$(count 'started:')" 0check "ASan 리포트 없음" no_asanecho "[$VAR] 3. 이미 쓰는 포트로 기동"occupy; mkcfg; "$BIN" "$W/d.cfg" 2> "$W/log" & PID=$!; wait_exit 3check "exit 2" eq "$RC" 2check "스레드를 만들기 전에 종료 (started 없음)" eq "$(count 'started:')" 0check "ASan 리포트 없음" no_asankill $BLOCK 2>/dev/null; wait $BLOCK 2>/dev/nullecho "[$VAR] 4. 부하 중 SIGTERM"mkcfg; start( for i in $(seq 1 2000); do send "$VALID"; done ) & LOAD=$!sleep 0.5; kill -TERM $PID; wait_exit 5; wait $LOAD 2>/dev/nullR=$(count 'recv session'); D=$(count 'done session')check "exit 0" eq "$RC" 0check "처리한 것이 1건 이상" test "$R" -ge 1check "recv == done (큐에 남은 것까지 처리, 유실 0): $R / $D" eq "$R" "$D"check "ASan 리포트 없음" no_asanecho "[$VAR] 5. 쓰레기 패킷 100개"mkcfg; startfor i in $(seq 1 100); do send "$(head -c 20 /dev/urandom | od -An -v -tx1 | sed 's/ /\\x/g' | tr -d '\n')"; donesleep 0.5check "프로세스 생존" kill -0 $PIDcheck "WRN 로그가 남음" test "$(count 'WRN')" -ge 1send "$VALID"; sleep 0.5check "이후 정상 메시지는 처리됨" test "$(count 'done session')" -ge 1kill -TERM $PID; wait_exit 5check "exit 0" eq "$RC" 0echo "[$VAR] 6. SIGHUP — 로그 레벨만 바뀐다"mkcfg 4 INF; startsend "$VALID"; sleep 0.4; before=$(count 'recv session')mkcfg 4 ERRkill -HUP $PID; sleep 0.5send "$VALID"; sleep 0.4check "HUP 후에는 INF 로그가 안 나옴" eq "$(count 'recv session')" "$before"check "프로세스 생존 (무중단)" kill -0 $PIDkill -TERM $PID; wait_exit 5check "exit 0" eq "$RC" 0echo "[$VAR] 7. kill -9 (비교용)"mkcfg; start; kill -9 $PID; wait $PID 2>/dev/null; RC=$?check "137 (정리 못 하는 게 정상)" eq "$RC" 137if [ "$VAR" = B ]; then echo "[B] B-a. 메시지가 반만 온다 → 나머지가 오면 처리" mkcfg; start { exec 3<>/dev/tcp/127.0.0.1/$PORT printf '\x22\x85\x00\x00\x01' >&3; sleep 0.3 printf '\x02\x03\x04\x0a\x0b\x0c\x90\xc2\xd9' >&3; sleep 0.4; exec 3>&-; } 2>/dev/null check "반 + 반 → done 1건" eq "$(count 'done session')" 1 echo "[B] B-b. 두 클라이언트가 섞여서 보낸다" { exec 3<>/dev/tcp/127.0.0.1/$PORT; exec 4<>/dev/tcp/127.0.0.1/$PORT printf '\x22\x85\x00\x00\x01\x02\x03' >&3; sleep 0.2 printf "$VALID" >&4; sleep 0.2 printf '\x04\x0a\x0b\x0c\x90\xc2\xd9' >&3; sleep 0.4; exec 3>&- 4>&-; } 2>/dev/null check "각자 한 건씩 → done 이 3건 (앞의 1건 포함)" eq "$(count 'done session')" 3 echo "[B] B-c. 연결 · 끊김 300번 → fd 가 새지 않는다" for i in $(seq 1 300); do { exec 3<>/dev/tcp/127.0.0.1/$PORT; exec 3>&-; } 2>/dev/null; done sleep 0.5 check "프로세스 생존" kill -0 $PID check "열린 fd 20개 이하: $(ls /proc/$PID/fd 2>/dev/null | wc -l)" test "$(ls /proc/$PID/fd 2>/dev/null | wc -l)" -le 20 echo "[B] B-d. 메시지 도중 끊기" { exec 3<>/dev/tcp/127.0.0.1/$PORT; printf '\x22\x85\x00' >&3; exec 3>&-; } 2>/dev/null sleep 0.4 check "프로세스 생존" kill -0 $PID kill -TERM $PID; wait_exit 5 check "exit 0" eq "$RC" 0 check "ASan 리포트 없음" no_asanfiif [ "$VAR" = C ]; then echo "[C] C-a. 무응답 요청은 expire_sec 뒤에 만료된다" mkcfg 4 INF 1; start send "$NOREPLY"; send "$VALID"; sleep 2.6 check "expired 1건 (NOREPLY 만)" eq "$(count 'expired session')" 1 check "정상 요청은 만료되지 않음 (done 1건 이상)" test "$(count 'done session')" -ge 1 echo "[C] C-b. 무응답 요청 100개" for i in $(seq 1 100); do send "$NOREPLY"; done sleep 3 check "만료 수 == recv - done (무응답은 done 이 없다)" eq "$(count 'expired session')" "$(( $(count 'recv session') - $(count 'done session') ))" kill -TERM $PID; wait_exit 5 check "exit 0" eq "$RC" 0 echo "[C] C-c. 대기 중인 슬롯이 있어도 종료는 5초 안에 된다" mkcfg 4 INF 30; start send "$NOREPLY"; sleep 0.3 t0=$SECONDS; kill -TERM $PID; wait_exit 5 check "exit 0" eq "$RC" 0 check "5초 이내" test $((SECONDS - t0)) -le 5 check "ASan 리포트 없음 (슬롯 · 감시 스레드 정리)" no_asanfiecho "pass=$pass fail=$fail"if [ "$fail" -eq 0 ]; then echo PASS; else echo "FAIL $fail"; exit 1; fi
이 스크립트도 먼저 깨뜨려 본다
시나리오는 데몬이 틀렸을 때 X 가 나와야 의미가 있다.
아래처럼 일부러 틀리게 만들고 X 가 나오는지 확인한다.
# ① 큐 단위 시험 (먼저)make D=d7_daemon V=jobq SRC="d7_daemon/src/jobq.c d7_daemon/test/test_jobq.c"# ② 데몬 빌드 → 시나리오 시험make D=d7_daemon V=daemon SRC="$(ls d7_daemon/src/*.c)" build/d7_daemon_daemonbash d7_daemon/test/run.sh build/d7_daemon_daemon A # 변형 A (UDP)bash d7_daemon/test/run.sh build/d7_daemon_daemon B # 변형 B (TCP)bash d7_daemon/test/run.sh build/d7_daemon_daemon C # 변형 C (타임아웃 감시)
jobq_close() 를 join 전에 부른다.
안 부르면 워커가 cond_wait 에서 영원히 안 깬다 — 전형적인 종료 행(hang).
poll 타임아웃 200ms 는 깃발 확인 주기다.
-1(무한)로 두면 조용한 시간대에 종료가 안 된다.
종료 대기 상한 5초 — 안 끝나면 포기한다
/* join 을 무한히 기다리지 않으려면 워커가 "나 끝났다"를 카운트로 알린다 */static int wait_workers(jobq_t *q, int expect, int timeout_sec){ struct timespec ts; int ret = RET_OK; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += timeout_sec; pthread_mutex_lock(&q->lock); while (q->done_num < expect) { if (pthread_cond_timedwait(&q->all_done, &q->lock, &ts) == ETIMEDOUT) { LOG_WRN("shutdown timeout: %d/%d done", q->done_num, expect); ret = RET_TIMEOUT; /* 남은 작업은 포기하고 진행 */ break; } } pthread_mutex_unlock(&q->lock); return ret;}